iT邦幫忙

2022 iThome 鐵人賽

DAY 26
1
Mobile Development

新手向Android&Kotlin學習紀錄30天系列 第 26

第26天 Kotlin小學堂(15) :標準函數

  • 分享至 

  • xImage
  •  

標準函數在做什麼

Kotlin的標準函數(或叫Scope functions)是指在Standard.kt中定義的函數,在任何Kotlin程式碼都可以自由呼叫他們:let、apply、with、also、run、takeIf和takeUnless等函數,適當使用這些函數可以幫助我們減少一些重複的程式碼。

基本上,這些函數的作用相同:在對象上執行一段程式碼。不同的是這個對象如何在作用域{}中變得可用,以及整個表達式的結果是什麼。

標準函數間有什麼不同

因為標準函數在本質上都非常相似,所以了解它們之間的差異很重要。每個標準函數之間有兩個主要區別:

  • 引用上下文對象的方式。在標準函數的 lambda 內部,上下文對象可通過短引用而不是其實際名稱獲得。每個標準函數使用以下兩種方式之一來訪問對象:

    • 作為lambda接收者( this) : 對象作為接收者在lambda表達式中,可以讓所有函數作用在接收者上,所以this通常可以省略不寫。(apply、with、run)
    • 作為lambda參數(it)。 : 對象作為lambda參數傳入,用it代替對象的名稱,不可省略。(let、also、takeIf、takeUnless)
  • 返回值:

    • Context object : 返回對象本身。(also、apply、takeIf、takeUnless)
    • Lambda result : 可以將此結果分配給變量、對結果進行鏈接操作。(run、let、with)

run

  • 短引用 : this
  • 返回值 : Lambda result
  • 在同時具有對象初始化及需要計算的返回值時很有用
    例:檢查帳號長度必須小於等於10
fun main(){
    val username = "ironman"
    // 不使用run,也許會這樣寫
    val isIllegalName = isTooLong(username)
    val result = check(isIllegalName)
    print(result) //結果: 註冊成功
    
    //使用run
    username.run(::isTooLong)
            .run(::check)
            .run(::println)
            //結果: 註冊成功
  
    //使用run巢狀寫法
    println(check(isTooLong(username)))  //結果: 註冊成功
    
}
//檢查長度
fun isTooLong(name : String) = name.length > 10
//判斷要印出的訊息
fun check(isTooLong : Boolean): String {
    return if (isTooLong) {
        "名字太長啦!"
    } else {
        "註冊成功"
    }
}  

let

前面文章曾配合安全呼叫來檢處理可空類型,這也是官方指南(在文章最後)建議的功能。這裡簡單講一下它的特性:

  • 短引用 : it
  • 返回值 : Lambda result
  • 對象為空就不執行
  • 可以被使用在呼叫1個或多個函數的呼叫鏈中,如下例:
    //取numbers中元素的奇數平方值印出:
    val numbers = listOf<Int>(1, 2, 3, 4, 5)
    
    //使用的let的寫法:
    numbers.filter { it % 2 == 1 }.map { it * it }.let{println(it)} 
   
    //不使用的let的寫法:
    val resultList = numbers.filter { it % 2 == 1 }.map { it * it }
    println(resultList)

如果let內只有一個用it作為參數的函數時,也可以使用::引用方法,如: let{println(it)}可寫成let(::println)

apply

  • 短引用 : this
  • 返回值 : Context object
  • 如果想對一個可能為null的對象同時操作多個成員屬性或函數,而不是想要一個返回值的話,apply是很適合的。對象如果是null就不會執行。
    例:
  1. 先建個Person類別 : Person類別會記錄幾歲住在哪裡等資料。並有MoveTo()來執行搬家,搬家後會印出幾歲搬去哪,當然年紀也會grow up
//主建構函數預設0歲(這裡不討論胎兒)並居無定所
class Person(val name: String, var age: Int = 0, var neighborhood: String = "居無定所") {
    //搬到哪去
    fun moveTo(place: String) {
        neighborhood = place
        println("$age \b歲的$name \b搬到$neighborhood")
    }
    //長大了幾歲
    fun grownUp(increase: Int) {
        age += increase
    }
}
  1. 來說個孟母三遷的故事
fun main(){
    //孟子沒人不知道吧,設定基本資料時,可以使用apply
    val mencius = Person("孟子", 3 , "墳地附近")
       
     //大家也都知道他媽媽出了名的愛搬家吧,年紀我亂掰的
     //年紀 、住處資料變更時可以使用apply 
    mencius.apply {
        println("$age \b歲的$name \b搬到$neighborhood")
        grownUp(1)
        moveTo("市集屠宰場附近")
        grownUp(1)
        moveTo("學堂旁")
    }    
    //結果:3歲的孟子搬到墳地附近
    //    4歲的孟子搬到市集屠宰場附近
    //    5歲的孟子搬到學堂旁
}

不使用apply的話,就要一直喊孟子出來:

   println("$age \b歲的$name \b住在$neighborhood")
   mencius.grownUp(1)
   mencius.moveTo("市集屠宰場附近")
   mencius.grownUp(1)
   mencius.moveTo("學堂旁")
   //執行結果同上。

with

  • 短引用 : this
  • 返回值 : Lambda result
  • with是run的變形,行為功能都一樣,但呼叫方式不同,with要求對象傳入當第一個參數,但是在lambda中可以當作接收者(this)
  • 要注意傳入的對象不可為空,因為with照樣會將null傳入lambda。
    kotlin官方建議,不使用Lambda返回值,單純以「和這個對象做接下來的事」這樣使用
    例:
    val numbers = listOf<Int>(1, 2, 3, 4, 5)
    with(numbers){
        println("numbers中有: $this")
        println("numbers中共有 $size 個元素")
    }

另一種用例是引入一個輔助對象,對象的屬性或函數可以用於計算出一個返回值

val numbers = listOf<Int>(1, 2, 3, 4, 5)

    val result = with(numbers) {
        //使用「+」運算子連接兩個句子
        "numbers中第一個元素 : ${first()}\n" +
        "numbers中最後一個元素 : ${last()}"
    }
    println(result)
    //結果:
    // numbers中第一個元素 : 1
    // numbers中最後一個元素 : 5

因為with使用方法比較不同,而且還要注意對象可空的問題,所以好像比較少用(?)

also

  • 短引用 : it
  • 返回值 : Context object
  • 適合對這個引用對象本身做操作,而不是他的屬性或函數。
  • 不想對這個對象使用apply那樣隱式呼叫(this)時
  • 可以理解成「也對這個對象做這些事」,像是順便一起做的樣子。
    例:numbers宣告後,先印出來然後再加一個元素6進去。
    因為also 是回傳對象本身,所以後面才能接著呼叫add()
fun main(){
    val numbers = mutableListOf<Int>(1, 2, 3, 4, 5)    
    numbers.also { println(it) }.add(6)
    //結果: [1, 2, 3, 4, 5]
}

takeIf

takeIf函數會判斷lambda中提供的條件運算式,若為true就會返回接收者物件,false返回null。
下例是一個隨機數字如果是大於40的偶數才會印出:

    val randomNum = (1..100).random()
    
    //由於takeIf有可能返回null值,所以使用安全呼叫
    randomNum.takeIf { it > 40 && it % 2 == 0 }?.let(::println)

takeUnless

takeIf的輔助性函數,兩者唯一區別是takeUnless是判斷給訂條件為false的時候才回返回原始接收者物件。如果是複雜的判斷建議還是使用takeIf比較好,語意上比較好讀不易混淆。

關鍵差異整理

Function Object reference Return value Is extension function
let it Lambda result Yes
run this Lambda result Yes
run - Lambda result No: called without the context object
with this Lambda result No: called without the context object
apply this Context object Yes
also this Context object Yes
takeIf it Context object/null Yes
takeUnless it Context object/null Yes

註 : run函數有另一個不常用的版本是不需要接收者,不傳遞接收者引數,沒有作用域限制,返回lambda值。

官方選用指南:

  • 在非空對像上執行 lambda:let
  • 在局部範圍內將表達式作為變數引入:let
  • 對象配置:apply
  • 對象配置和計算結果:run
  • 在需要表達式的地方運行語句:非擴展函數版的run
  • 附加效果:also
  • 對對象進行分組函數調用:with
    不同功能的用例重疊,因此您可以根據項目或團隊中使用的特定約定來選擇功能。
    儘管標準函數是一種使代碼更簡潔的方法,但請避免過度使用它們:它會降低代碼的可讀性並導致錯誤。避免嵌套標準函數並在鏈接它們時要小心:很容易對當前上下文對象和this或it的值感到困惑。

參考
kotlin權威2.0
kotlin官方文檔
Khan Academy’s - Nice utility functions
明天見


上一篇
第25天 Kotlin小學堂(14) : Lateinit & Lazy properties
下一篇
第27天 Kotlin小學堂(16) : 抽象類別與介面
系列文
新手向Android&Kotlin學習紀錄30天30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言